--
-- Author: shunguo.chen
-- Date: 2017-02-22 14:38:27
--
-- lsnapshot lua内存快照
--

local List = {}

function List.new ()
    return {first = 0, last = -1}
end

function List.push (list, value)
    local last = list.last + 1
    list.last = last
    list[last] = value
end

function List.pop (list)    
    local first = list.first
    if first > list.last then error("list is empty") end
    local value = list[first]
    list[first] = nil        
    list.first = first + 1
    return value
end

function List.isempty (list)
    return list.first > list.last
end


local _M = {}
_M.obj_map = nil
_M.ignore_map = {}
setmetatable(_M.ignore_map, {__mode = "k"})
_M.ignore_map[_M.ignore_map] = true

function _M:snapshot()
    self.obj_num = 0
    self.obj_map = {}
    setmetatable(self.obj_map, {__mode = "k"})

    self:add_ignore(self)
    self:add_ignore(self.obj_map)

    -- local registy = debug.getregistry()
    local registy = _G
    for k,v in pairs(registy) do
        if k == "tolua_super" then

        else
            self:travel(k, v, nil)
        end
    end

    return self.obj_map, self.obj_num
end

local function need_travel(type)
    return type == "table" or type == "function" or type == "userdata" or type == "thread" 
    -- return type == "table"
end

local function get_key(key)
    local k_type = type(key)
    if k_type == "string" or k_type == "number" then
        return tostring(key)
    else
        return "[" .. k_type .. "]"
    end
end

function _M:get_data(obj)
    if nil == obj then return nil end
    local data = self.obj_map[obj]
    if not data then
        local refs = {}
        setmetatable(refs, {__mode = "k"})
        data = {name="", child=0, mark=false, refs=refs, refnum=0}
        self.obj_num = self.obj_num + 1
        self.obj_map[obj] = data

        self:add_ignore(data)
        self:add_ignore(refs)
    end
    return data
end

function _M:add_ignore(obj)
    if obj then
        self.ignore_map[obj] = true
    end
end

function _M:is_ignore(obj)
    if obj then
        return self.ignore_map[obj]
    else
        return false
    end
end

function _M:travel(k, v, parent, how)
    local k_type = type(k)
    local v_type = type(v)

    local is_lshot = k_type == "string" and string.find(k, "app.public.luatraverse.luashot")
    if is_lshot then return end

    if not self:is_ignore(k) and need_travel(k_type) then
        self:update_ref(nil, k, parent, "key")
        self["travel_"..k_type](self, nil, k, parent, "key")
    end
    if not self:is_ignore(v) and need_travel(v_type) then
        self:update_ref(k, v, parent, how or "value")
        self["travel_"..v_type](self, k, v, parent, how or "value")
    end
end

function _M:update_ref(key, obj, parent, how)
    local data = self:get_data(obj)
    local p_data = self:get_data(parent)
    local p_name = p_data and p_data.name or ""

    local ref
    if how == "value" then
        ref = p_name .. "/" .. get_key(key)
        data.name = ref
    elseif how == "meta" then
        ref = string.format("%s/[%s]", p_name, how)
        data.name = ref
    else
        ref = string.format("%s/[%s]", p_name, how)
    end
    if obj ~= _G and #p_name > 0 then
        data.refs[parent] = ref
        data.refnum = data.refnum + 1
    end
end

function _M:travel_table(key, obj, parent, how)
    -- print("travel_table")
    local data = self:get_data(obj)

    local can = true
    if can and not data.mark then
        data.mark = true

        for k,v in pairs(obj) do
            local k_type = type(k)
            if k_type == "string" and 
                (string.find(k, "^__") ~= nil or string.find(k, "^tolua_") ~= nil) then

            else
                self:travel(k, v, obj, nil)
            end
            data.child = data.child + 1
        end

        local mtable = debug.getmetatable(obj)
        self:travel(nil, mtable, obj, "meta")
    end
end

function _M:travel_function(key, obj, parent, how)
    -- print("travel_function")
    local data = self:get_data(obj)

    local info = debug.getinfo(obj, "nSl")
    self:add_ignore(info)
    -- dump(info, "travel_function info")
    local path
    if info.what == "Lua" then
        path = info.short_src .. ":" .. info.linedefined
    end
    if not data.path then
        data.path = path
    end

    local can = true
    if can and not data.mark then
        data.mark = true

        -- gets the upvalues
        local i = 1 
        while true do
            local n, v = debug.getupvalue(obj, i)
            if not n then break end -- when there is no upvalues
            self:travel(n, v, obj, "up")
            i = i + 1
            -- data.child = data.child + 1
        end

        local fenv = debug.getfenv(obj)
        self:travel(nil, fenv, obj, "env")
    end
end

function _M:travel_userdata(key, obj, parent, how)
    -- print("travel_userdata")
    local data = self:get_data(obj)

    local can = true
    if can and not data.mark then
        data.mark = true

        local mtable = debug.getmetatable(obj)
        self:travel(nil, mtable, obj, "meta")

        local fenv = debug.getfenv(obj)
        self:travel(nil, fenv, obj, "env")
    end
end

function _M:travel_thread(key, obj, parent, how)
    -- print("travel_thread")
    local data = self:get_data(obj)

    local can = true
    if can and not data.mark then
        data.mark = true

        -- get local of active func
        for i=3, math.huge do
            local info = debug.getinfo(obj, i, "f") 
            self:add_ignore(info)
            if not info then break end 
            for j=1, math.huge do
                local n, v = debug.getlocal(obj, i , j)
                if not n then break end
                -- print("travel_thread i="..i, n, v)
                self:travel(n, v, obj, "value")
            end
        end

        local fenv = debug.getfenv(obj)
        self:travel(nil, fenv, obj, "env")
    end
end

-- ignore _M's function
for k,v in pairs(_M) do
    if type(v) == "function" then
        _M.ignore_map[v] = true
    end
end

_M.ignore_map[need_travel] = true
_M.ignore_map[get_key] = true

return _M